6. 内核模块实验案例

您所在的位置:网站首页 linux驱动开发实战指南 pdf 6. 内核模块实验案例

6. 内核模块实验案例

2024-07-17 23:30:48| 来源: 网络整理| 查看: 265

6. 内核模块实验案例¶ 6.1. helloworld实验¶

下面是实验代码讲解部分。

6.1.1. 示例代码¶

本节的示例代码目录为:linux_driver/helloworld

从前面章节我们已经知道了内核模块的工作原理,这一小节先以之前的helloworld驱动使用案例,讲解分析一下简单的内核模块代码框架。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32#include //包含宏定义的头文件 #include //包含初始化加载模块的头文件 //入口函数功能实现 static int hello_init(void) { //内核层只能使用printk,不能使用printf,因为内核层不支持C语言 printk(KERN_EMERG "[ KERN_EMERG ] Hello World Init\n"); //输出等级为0 printk("[ default ] Hello World Init\n"); return 0; } //出口函数功能实现 static void hello_exit(void) { printk(KERN_EMERG "[ KERN_EMERG ] Hello World Exit\n"); //输出等级为0 printk("[ default ] Hello World Exit\n"); } module_init(hello_init); //驱动入口 module_exit(hello_exit); //驱动出口 MODULE_LICENSE("GPL v2"); //声明开源许可证 // "GPL" 是指明 这是GNU General Public License的任意版本 // “GPL v2” 是指明 这仅声明为GPL的第二版本 // "GPL and addtional" // "Dual BSD/GPL" // "Dual MPL/GPL" // "Proprietary" 私有的 MODULE_AUTHOR("embedfire"); //声明作者信息 MODULE_DESCRIPTION("hello world"); //对这个模块作一个简单的描述 MODULE_ALIAS("hello world_test"); //这个模块的别名

接来下理解每一行代码的含义,并最终在Linux上运行这个模组,验证我们前面的理论,也为下一章驱动打下基础。

6.1.2. 代码框架分析¶

Linux内核模块的代码框架通常由下面几个部分组成:

模块加载函数(必须): 当通过insmod或modprobe命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。

模块卸载函数(必须): 当执行rmmod命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。

模块许可证声明(必须): 许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。

模块参数: 模块参数是模块被加载时,可以传值给模块中的参数。

模块导出符号: 模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。

模块的其他相关信息: 可以声明模块作者等信息。

上面示例的hello module程序只包含上面三个必要部分以及模块的其他信息声明(模块参数和导出符号将在下一节实验出现)。

6.1.2.1. 头文件¶

编写内核模块所需要的头文件,并不在/usr/include目录,而是在Linux内核源码中的include目录。

编写内核模块中经常要使用到的头文件有以下两个:和。我们可以看到在头文件前面也带有一个文件夹的名字linux,对应了include下的linux文件夹,我们到该文件夹下,查看这两个头文件都有什么内容。

init.h头文件(位于内核源码 /include/linux/init.h)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24#define pure_initcall(fn) __define_initcall(fn, 0) #define core_initcall(fn) __define_initcall(fn, 1) #define core_initcall_sync(fn) __define_initcall(fn, 1s) #define postcore_initcall(fn) __define_initcall(fn, 2) #define postcore_initcall_sync(fn) __define_initcall(fn, 2s) #define arch_initcall(fn) __define_initcall(fn, 3) #define arch_initcall_sync(fn) __define_initcall(fn, 3s) #define subsys_initcall(fn) __define_initcall(fn, 4) #define subsys_initcall_sync(fn) __define_initcall(fn, 4s) #define fs_initcall(fn) __define_initcall(fn, 5) #define fs_initcall_sync(fn) __define_initcall(fn, 5s) #define rootfs_initcall(fn) __define_initcall(fn, rootfs) #define device_initcall(fn) __define_initcall(fn, 6) #define device_initcall_sync(fn) __define_initcall(fn, 6s) #define late_initcall(fn) __define_initcall(fn, 7) #define late_initcall_sync(fn) __define_initcall(fn, 7s) #define __initcall(fn) device_initcall(fn) #define __exitcall(fn) \ static exitcall_t __exitcall_##fn __exit_call = fn #define console_initcall(fn) ___define_initcall(fn, con, .con_initcall)

注解

Init.h头文件主要包含了一些宏定义,还有内核的initcall机制。

module.h头文件(位于内核源码/include/linux/module.h)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13/* Generic info of form tag = "info" */ #define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info) /* For userspace: you can also call me... */ #define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias) #define MODULE_LICENSE(_license) MODULE_INFO(license, _license) #define MODULE_AUTHOR(_author) MODULE_INFO(author, _author) #define module_init(x) __initcall(x); #define module_exit(x) __exitcall(x);

注解

以上代码中,列举了module.h文件中有内核模块的加载、卸载函数的声明,还有一部分宏定义,有的是可有可无的,但是MODULE_LICENSE这个是指定该内核模块的许可证,是必须要有的。

6.1.2.2. 模块加载/卸载函数¶

module_init

回忆我们使用单片机时,假设我们要使用串口等外设时,是不是都需要调用一个初始化函数,在这个函数里面,我们初始化了串口的GPIO,配置了串口的相关参数,如波特率,数据位,停止位等等参数。

func_init函数在内核模块中也是做与初始化相关的工作。

内核模块入口函数¶ //下面是module_init()函数定义过程 //内核源码/include/linux/module.h #define module_init(x) __initcall(x); //内核源码/include/linux/init.h #define __initcall(fn) device_initcall(fn) #define device_initcall(fn) __define_initcall(fn, 6) #define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id) //底层定义 #else #define ___define_initcall(fn, id, __sec) \ static initcall_t __initcall_##fn##id __used \ __attribute__((__section__(#__sec ".init"))) = fn; #endif #define __init __section(.init.text) __cold __latent_entropy __noinitretpoline __nocfi #define __initdata __section(.init.data) //__init用于修饰函数,__initdata用于修饰变量。 //带有__init的修饰符,表示将该函数放到可执行文件的__init节区中,该节区的内容只能用于模块的 //初始化阶段,初始化阶段执行完毕之后,这部分的内容就会被释放掉,真可谓是“针尖也要削点铁”。 //下面是功能实现 //自行编写部分,如helloworld.c中的hello_init static int func_init(void) { } module_init(func_init);

func_init函数返回值:

0:表示模块初始化成功,并会在/sys/module下新建一个以模块名为名的目录。

非0:表示模块初始化失败

在C语言中,static关键字的作用如下:

static修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期。

static的修饰全局变量只能在本文件中访问,不能在其它文件中访问。

static修饰的函数只能在本文件中调用,不能被其他文件调用。

module_exit

理解了模块加载的内容之后,来学习模块卸载函数应该会比较简单。

与内核加载函数相反,内核模块卸载函数func_exit主要是用于释放初始化阶段分配的内存,分配的设备号等,是初始化过程的逆过程。

内核模块卸载函数¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19//下面是module_exit()函数定义过程 //内核源码/include/linux/module.h #define module_exit(x) __exitcall(x); //内核源码/include/linux/init.h #define __exitcall(fn) \ static exitcall_t __exitcall_##fn __exit_call = fn #define __exit __section(.exit.text) __exitused __cold notrace #define __exitdata __section(.exit.data) #define __exit_call __used __section(.exitcall.exit) //类比于模块加载函数,__exit用于修饰函数,__exitdata用于修饰变量。 //宏定义module_exit用于告诉内核,当卸载模块时,需要调用哪个函数。 //下面是功能实现 //自行编写部分,如helloworld.c中的hello_exit static void func_exit(void) { } module_exit(func_exit);

func_exit函数与func_init函数区别在于,该函数的返回值是void类型。

printk函数

printf:glibc实现的打印函数,工作于用户空间

printk:内核模块无法使用glibc库函数,内核自身实现的一个类printf函数,但是需要指定打印等级。

#define KERN_EMERG

“” 通常是系统崩溃前的信息

#define KERN_ALERT

“” 需要立即处理的消息

#define KERN_CRIT

“” 严重情况

#define KERN_ERR

“” 错误情况

#define KERN_WARNING

“” 有问题的情况

#define KERN_NOTICE

“” 注意信息

#define KERN_INFO

“” 普通消息

#define KERN_DEBUG

“” 调试信息

注解

查看当前系统printk打印等级:cat /proc/sys/kernel/printk,从左到右依次对应当前控制台日志级别、默认消息日志级别、最小的控制台级别、默认控制台日志级别。

6.1.2.3. 许可证¶

Linux是一款免费的操作系统,采用了GPL协议,允许用户可以任意修改其源代码。GPL协议的主要内容是软件产品中即使使用了某个GPL协议产品提供的库,衍生出一个新产品,该软件产品都必须采用GPL协议,即必须是开源和免费使用的,可见GPL协议具有传染性。因此,我们可以在Linux使用各种各样的免费软件。

许可证¶ 1#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)

注解

内核模块支持的许可证可查看代码注释。

6.1.2.4. 相关信息声明¶

下面,我们介绍一下关于内核模块程序结构的最后一部分内容。这部分内容只是为了给使用该模块的读者一本“说明书”,属于可有可无的部分,有则锦上添花,没有也无伤大雅。

内核模块信息声明函数:

函数

作用

MODULE_LICENSE()

表示模块代码接受的软件许可协议

MODULE_AUTHOR()

描述模块的作者信息

MODULE_DESCRIPTION()

对模块的简单介绍

MODULE_ALIAS()

给模块设置一个别名

作者信息

内核模块作者宏定义(位于内核源码/linux/module.h)¶ 1#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)

我们前面使用modinfo中打印出的模块信息中“author”信息便是来自于宏定义MODULE_AUTHOR。该宏定义用于声明该模块的作者。

模块描述信息

模块描述信息(位于内核源码/linux/module.h)¶ 1#define MODULE_DESCRIPTION(_description) MODULE_INFO(description, _description)

模块信息中“description”信息则来自宏MODULE_DESCRIPTION,该宏用于描述该模块的功能作用。

模块别名

内核模块别名宏定义(位于内核源码/linux/module.h)¶ 1#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)

模块信息中“alias”信息来自于宏定义MODULE_ALIAS。该宏定义用于给内核模块起别名。

注意

在使用该模块的别名时,需要将该模块复制到/lib/modules/内核源码/下,使用命令depmod更新模块的依赖关系,否则的话,Linux内核怎么知道这个模块还有另一个名字。

6.1.3. Makefile文件说明¶

关于Makefile文件说明,请参考 《内核模块的编译》 内容。

6.1.4. 程序运行结果¶

该案例请参考开发环境搭建章节的 《搭建文件传输环境》 内容,提前搭建好请NFS环境请搭建。也可以直接scp命令进行传输。

在服务器/虚拟机上编译:

cd linux_driver/helloworld/ #清除生成的文件 make clean #开始编译 make

传输到鲁班猫板卡上运行:

#SCP传输 scp helloworld.ko [email protected]:/home/cat/ #NFS文件系统 cp helloworld.ko /mnt/

打开鲁班猫终端,输入以下命令将helloworld驱动模块加载到内核。

insmod helloworld.ko

我们也可以输入以下命令,将 helloworld 驱动拆卸。

rmmod helloworld rmmod helloworld.ko

具体演示如下:

下面是一个小练习

#分别写入0~7到/proc/sys/kernel/printk echo 7 > /proc/sys/kernel/printk #查看写入不同等级时,加载和卸载模块终端打印的信息有啥区别 insmod helloworld.ko rmmod helloworld 6.2. 内核模块传参与符号共享实验¶

本节的示例代码目录为:linux_driver/moudle/moduleparam_test

6.2.1. 实验代码讲解¶ 6.2.1.1. 内核模块传参代码讲解¶

内核模块作为一个可拓展的动态模块,为Linux内核提供了灵活性,但是有时我们需要根据不同的应用场景给内核传递不同的参数,例如在程序中开启调试模式、设置详细输出模式以及制定与具体模块相关的选项,都可以通过参数的形式来改变模块的行为。

Linux内核提供一个宏来实现模块的参数传递。

module_param函数 (内核源码/include/linux/moduleparam.h)¶ 1 2 3 4#define module_param(name, type, perm) \ module_param_named(name, name, type, perm) #define module_param_array(name, type, nump, perm) \ module_param_array_named(name, name, type, nump, perm)

以上代码中的module_param函数需要传入三个参数:

name: 我们定义的变量名

type: 参数的类型

perm: 表示的是该文件的权限,具体参数值见下表

小技巧

目前内核支持的参数类型有byte,short,ushort,int,uint,long,ulong,charp,bool,invbool。

其中charp表示的是字符指针,bool是布尔类型,其值只能为0或者是1;invbool是反布尔类型,其值也是只能取0或者是1,但是true值表示0,false表示1。变量是char类型时,传参只能是byte,char * 时只能是charp。

下面是我们的实验代码:

示例程序¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21static int itype=0; module_param(itype,int,0); static bool btype=0; module_param(btype,bool,0644); static char ctype=0; module_param(ctype,byte,0); static char *stype=0; module_param(stype,charp,0644); static int __init param_init(void) { printk(KERN_ALERT "param init!\n"); printk(KERN_ALERT "itype=%d\n",itype); printk(KERN_ALERT "btype=%d\n",btype); printk(KERN_ALERT "ctype=%d\n",ctype); printk(KERN_ALERT "stype=%s\n",stype); return 0; }

第1-11行:定义了四个常见变量然后使用module_param宏来声明这四个参数

第13-21行:并在param_init中输出上面声明的四个参数。

6.2.1.2. 符号共享代码讲解¶

在前面我们已经详细的分析了关于导出符号的内核源码,符号指的就是在内核模块中导出函数和变量,在加载模块时被记录在公共内核符号表中,以供其他模块调用。

这个机制,允许我们使用分层的思想解决一些复杂的模块设计。我们在编写一个驱动的时候,可以把驱动按照功能分成几个内核模块,借助符号共享去实现模块与模块之间的接口调用,变量共享。

导出符号 (内核源码/include/linux/export.h)¶ 1 2#define EXPORT_SYMBOL(sym) __EXPORT_SYMBOL(sym, "") //EXPORT_SYMBOL宏用于向内核导出符号,这样的话,其他模块也可以使用我们导出的符号了。

下面通过一段代码,介绍如何使用某个模块导出符号。

moduleparam_test.c¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20//...省略代码... static int itype=0; module_param(itype,int,0); EXPORT_SYMBOL(itype); int my_add(int a, int b) { return a+b; } EXPORT_SYMBOL(my_add); int my_sub(int a, int b) { return a-b; } EXPORT_SYMBOL(my_sub); //...省略代码...

第2-5行:定义了参数itype,并通过EXPORT_SYMBOL宏导出

第7-12行:定义my_add函数,并通过EXPORT_SYMBOL宏导出

第14-29行:定义my_sub函数,并通过EXPORT_SYMBOL宏导出

注解

以上代码中,省略了内核模块程序的其他内容,如头文件,加载/卸载函数等。

calculation.h¶ 1 2 3 4 5 6 7 8 9#ifndef __CALCULATION_H__ #define __CALCULATION_H__ extern int itype; int my_add(int a, int b); int my_sub(int a, int b); #endif

注解

声明额外的变量itype,my_add和my_sub函数。

calculation.c¶ 1 2 3 4 5 6 7 8 9 10 11//...省略代码... #include "calculation.h" //...省略代码... static int __init calculation_init(void) { printk(KERN_ALERT "calculation init!\n"); printk(KERN_ALERT "itype+1 = %d, itype-1 = %d\n", my_add(itype,1), my_sub(itype,1)); return 0; } //...省略代码...

注解

calculation.c中使用extern关键字声明的参数itype,调用my_add()、my_sub()函数进行计算。

6.2.2. 实验准备¶

获取内核模块源码,将配套代码linux_driver/放到内核代码同级目录,然后进入linux_driver/moudle/moduleparam_test目录中。

6.2.2.1. Makefile说明¶ Makefile (位于linux_driver/moudle/moduleparam_test/Makefile)¶ 1 2 3...省略代码... obj-m := parametermodule.o calculation.o ...省略代码...

注解

以上Makefile与上一个helloworld实验,只有目标文件不同。

6.2.2.2. 编译¶

在实验目录下输入如下命令来编译驱动模块:

cd linux_driver/moudle/moduleparam_test make clean make

编译成功后,实验目录下会生成名为 “moduleparam_test.ko” 和 “calculation.ko” 的驱动模块文件

6.2.3. 程序运行结果¶

通过NFS将编译好的 “moduleparam_test.ko” 和 “calculation.ko” 拷贝到开发板中,加载 moduleparam_test.ko 并传参,这时我们声明的四个变量的值,就是变成了我们赋的值。

sudo insmod moduleparam_test.ko itype=123 btype=1 ctype=200 stype=abc

查看向内核导出的符号表。

cat /proc/kallsyms | grep my_add cat /proc/kallsyms | grep my_sub

下面演示有依赖的内核模块加载。

1 2 3 4 5 6 7#将内核模块复制板卡到/lib/modules/5.4.125/kernel/目录 cp calculation.ko moduleparam_test.ko /lib/modules/5.4.125/kernel/ cd /lib/modules/5.4.125/ #然后执行 ``depmod -a`` depmod -a #查看modules.dep配置文件可以发现calculation.ko依赖moduleparam_test.ko。 cat modules.dep | grep calculation

执行modprobe calculation命令自动将会moduleparam_test.ko模块加载

1modprobe calculation 6.2.4. 附加练习¶ 6.2.4.1. 练习1¶

注解

如果把calculation模块和moduleparam_test模块都卸载后,直接执行 modprobe calculation 会怎样。

6.2.4.2. 练习2¶

注解

如果把calculation模块和moduleparam_test模块都卸载后,直接执行 insmod calculation.ko 加载模块会怎样。

6.3. CH341/CH340驱动实验¶

这一小节演示如何将CH341/CH340芯片(USB转串口芯片)的驱动编译成模块并加载。

6.3.1. 驱动源码获取¶

首先我们要先了解驱动源码获取的步骤:

先去内核源码里面去搜索,如果有的话,就直接选择这个驱动,然后直接使用

假如没有这个驱动,我们可以到设备/芯片官网,查找有没有对应的驱动,下载回来自行编译,然后加载到内核里面去运行

假如你千方百计都拿不到驱动代码,就只能自己编写一个驱动了。

6.3.1.1. 内核源码中查找¶

我们开源进入内核源码目录下,执行以下命令进入menuconfig配置页面

1make ARCH=arm64 menuconfig

然后输入 / 加上你像搜索的驱动名称,比如 CH34。

小技巧

搜索关键词尽量覆盖大范围,比如直接搜索 CH340 可能就搜不出来。

搜索结果如下:

这里可以看到内核源码里是有相应驱动的,为了客观的演示,后面将从内核源码中将驱动源码复制出来进行讲解。

6.3.1.2. 在网络上查找¶

网上搜驱动这一步需要技巧,我看到很多刚学嵌入式的同学,网上找个CH340串口模块的Windows驱动,找了好久才找到。

关于驱动的查找,一般去官网就有,比如CH341/CH340的驱动,可以去沁恒官网找“USB转UART”,找到对应型号接口芯片的Linux驱动进行下载。

6.3.1.3. 将源码拷贝到驱动练习目录¶

打开服务器/虚拟机终端。

1 2 3 4 5 6#在module目录下新建一个文件夹 mkdir CH341 && cd CH341 #拷贝CH341源码到刚才新建的目录 cp ../../../kernel/drivers/usb/serial/ch341.c . #拷贝上一个练习的Makefile文件到当前目录 cp ../moduleparam_test/Makefile . 6.3.2. 修改Makefile文件¶

下面为Makefile文件修改前内容。

Makefile¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15KERNEL_DIR=../../../kernel/ ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- export ARCH CROSS_COMPILE obj-m := ch341.o all: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules .PHONE:clean clean: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

标黄部分为修改部分, “obj-m := ch341.o ” 表示把ch341.o文件编译成模块,并生成一个独立的 “ch341.ko” 文件。

6.3.3. 编译内核模块¶

修改完需要指明编译架构和编译器进行编译即可,当出现“ch341.ko”文件,说明编译模块成功了。

1 2#编译 make 6.3.4. 加载CH341/CH340驱动模块¶

将编译出来的“ch341.ko”传输到鲁班猫板卡。

1 2 3 4 5 6 7 8 9 10#加载CH341/CH340驱动模块 insmod ch341.ko #加载时报错 insmod: ERROR: could not insert module ch341.ko: Unknown symbol in module #查看依赖的模块 modinfo ch341.ko |grep depends #加载缺少的依赖 sudo modprobe usbserial #重新加载驱动模块 insmod ch341.ko


【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭